Skip to content

dgb(stratum): emit only the won-block body the frozen header commits to (G3b bad-txnmrklroot) - #815

Merged
frstrtr merged 1 commit into
masterfrom
dgb/stratum-witness-commitment-fix
Jul 23, 2026
Merged

dgb(stratum): emit only the won-block body the frozen header commits to (G3b bad-txnmrklroot)#815
frstrtr merged 1 commit into
masterfrom
dgb/stratum-witness-commitment-fix

Conversation

@frstrtr

@frstrtr frstrtr commented Jul 23, 2026

Copy link
Copy Markdown
Owner

Closes the G3b bad-txnmrklroot reject on the empty-embedded-chain won block.

Root cause (corrected). The reconstruct in mining_submit() hand-rolled the block framing and could emit a body the ALREADY-HASHED header does not commit to. Neither divergence can be fixed by re-deriving the header — the miner hashed the merkle root, so any post-hoc coinbase mutation invalidates the PoW binding:

  1. tx-set vs commitment — the header merkle root is the ascent of the frozen stratum merkle branches from the coinbase txid. With no branches in the job (the empty-chain case: get_stratum_merkle_branches() is still a Stage-4c stub returning {}) the committed root is the bare coinbase txid — the miner committed to a coinbase-only block. Appending job->tx_data anyway is a deterministic bad-txnmrklroot.
  2. BIP144 witness form vs BIP141 commitment — marker/flag + reserved witness item were inserted on segwit_active alone. A witness-bearing coinbase with no aa21a9ed commitment output is rejected unexpected-witness, and the PPLNS connection coinbase only carries that output when the producer seam populates segwit_commitment_script (the empty-chain path does not). The commitment cannot be injected at submit time — it changes the coinbase txid. That correction supersedes the earlier "inject via add_witness_commitment at reconstruct" plan; injection belongs at job build (coinb2), a follow-on producer-seam slice.

Fix. New pure byte-level SSOT src/impl/dgb/coin/won_block_serialize.hpp: body == committed tx set; witness form IFF the coinbase carries the commitment, legacy otherwise (a coinbase-only legacy block is valid on a segwit-active chain). Both degradations log loudly naming the upstream seam to populate — the block ships degraded-but-valid rather than complete-but-rejected — and the won-block wire bytes are now dumped so any daemon-side reject is decodable offline without a second live repro.

Evidence. dgb_won_block_serialize_test 7/7 (both invariants + the structural guarantee that BIP144 reserialize leaves the txid preimage byte-identical), dgb_work_source_test 31/31, c2pool-dgb links. Registered in the build.yml target allowlist (#143 NOT_BUILT trap).

Scope. src/impl/dgb only, isolation-clean. p2pool-merged-v36 surface: none — parent-block (daemon-facing) framing, not share format / PoW / PPLNS.

Follow-ons (not this PR): populate segwit_commitment_script + coinbase_script (BIP34 height) at the main_dgb.cpp:861 producer seam — both are currently left unset, so the coinbase also carries no BIP34 height push (bad-cb-height would surface next, after the merkle root check passes); and un-stub the branch cache to claim body-tx fees.

The G3b won block on the empty-embedded-chain path was rejected
bad-txnmrklroot. The reconstruct in mining_submit() hand-rolled the block
framing and could emit a body the ALREADY-HASHED header does not commit
to. Two divergences, neither of which the header can be re-derived around
(the miner hashed the merkle root; any post-hoc coinbase mutation
invalidates the PoW binding):

1. TX-SET vs COMMITMENT. The header merkle root is the ascent of the
   frozen stratum merkle branches from the coinbase txid. With NO branches
   in the job -- the empty-chain case, where get_stratum_merkle_branches()
   is still a Stage-4c stub returning {} -- the committed root IS the bare
   coinbase txid, i.e. the miner committed to a coinbase-only block.
   Appending job->tx_data anyway is a deterministic bad-txnmrklroot.

2. BIP144 WITNESS FORM vs BIP141 COMMITMENT. The marker/flag + reserved
   witness item were inserted on segwit_active alone. A witness-bearing
   coinbase without the aa21a9ed commitment output is rejected
   (unexpected-witness), and the PPLNS connection coinbase only carries
   that output when the producer seam populates segwit_commitment_script
   -- which the empty-chain path does not. The commitment CANNOT be
   injected at submit time: it changes the coinbase txid.

Routes the framing through a new pure byte-level SSOT,
coin/won_block_serialize.hpp: body == committed tx set, witness form IFF
the coinbase carries the commitment, legacy otherwise (a coinbase-only
legacy block is valid on a segwit-active chain). Both degradations are
logged loudly with the upstream seam to populate -- the block ships
degraded-but-valid instead of complete-but-rejected -- and the won-block
wire bytes are now dumped so any daemon-side reject is decodable offline
without a second live repro.

KAT: 7 cases pinning both invariants plus the structural guarantee that
the BIP144 reserialize leaves the txid preimage byte-identical.
dgb_won_block_serialize_test 7/7, dgb_work_source_test 31/31, c2pool-dgb
links. Registered in the build.yml target allowlist (#143 NOT_BUILT trap).

Scope: src/impl/dgb only. p2pool-merged-v36 surface: none -- parent-block
framing, not share format / PoW / PPLNS.
@frstrtr
frstrtr merged commit 5f82e32 into master Jul 23, 2026
65 of 70 checks passed
@frstrtr
frstrtr deleted the dgb/stratum-witness-commitment-fix branch July 23, 2026 15:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant